package com.ar4j.spring;

import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;

import com.ar4j.ActiveRecord;
import com.ar4j.IActiveRecordFactory;

/**
 * A bean post processor that is able to autowire SpringActiveRecordAdapter instances.
 * A special case of autowiring is needed since Spring's main autowiring process does not detect 
 * generic types.
 */
@Component
public class AdapterInjectingBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
  
  @Autowired
  private IActiveRecordFactory factory;
  
  private Map<Class<?>, List<Member>> injectedMemberCache = Collections.synchronizedMap(new HashMap<Class<?>, List<Member>>());
  private ReentrantLock memberCacheLock = new ReentrantLock();
  
  private Map<Class<?>, SpringActiveRecordAdapter<?>> adapterCache = Collections.synchronizedMap(new HashMap<Class<?>, SpringActiveRecordAdapter<?>>());
  private ReentrantLock adapterCacheLock = new ReentrantLock();

  /**
   * Scan the bean for the @AutowiredAdapter annotation and inject an adapter
   * where needed.
   */
  @Override
  public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
    
    // see if we need to inject a 
    List<Member> toInject = getInjectedMemberList(bean.getClass());
    if(toInject != null && !toInject.isEmpty()) {
      for(Member member : toInject) {
        try {
          if(member instanceof Field) {
             Field field = (Field) member;
             Object value = getAdapterToInject(field.getGenericType());
             ReflectionUtils.makeAccessible(field);
             field.set(bean, value);
          } else {
             Method method = (Method) member;
             Object value = getAdapterToInject(method.getGenericParameterTypes()[0]);
             ReflectionUtils.makeAccessible(method);
             method.invoke(bean, value);
          }
        } catch(Exception e) {
          throw new FatalBeanException("Could not create and inject a SpringActiveRecordAdapter", e);
        }
      }
    }
    
    return super.postProcessAfterInstantiation(bean, beanName);
  }
  
  /**
   * Creates an adapter for the given type. The type must be a ParamerizedType
   * with one actual type parameter that is a subclass of ActiveRecord.
   */
  @SuppressWarnings("unchecked")
  private SpringActiveRecordAdapter<?> getAdapterToInject(Type type) {
    
    // get active record base class
    if(!(type instanceof ParameterizedType)) {
      throw new IllegalArgumentException("Type must be a parameterized type: " + type);
    }
    
    ParameterizedType parameterizedType = (ParameterizedType) type;
    if(parameterizedType.getActualTypeArguments() == null || parameterizedType.getActualTypeArguments().length != 1) {
      throw new IllegalArgumentException("Parameterized type must have one actual type argument: " + parameterizedType);
    }
    
    Class<?> baseClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
    if(!ActiveRecord.class.isAssignableFrom(baseClass)) {
      throw new IllegalArgumentException("Type argument must be an active record implementor: " + baseClass);
    }
    
    SpringActiveRecordAdapter<?> out = adapterCache.get(baseClass);
    if(out == null) {
      adapterCacheLock.lock();
      try {
        out = adapterCache.get(baseClass);
        if(out == null) {
          out = new SpringActiveRecordAdapter(baseClass, factory);
          adapterCache.put(baseClass, out);
        }
      } finally {
        adapterCacheLock.unlock();
      }
    }
    
    return out;
  }

  /**
   * Collects a list of members that need injection of an adapter.
   * This version uses a concurrent cache of injected members for the given
   * class. The actual scan is performed by doGetInjectedMemberList(Class)
   */
  private List<Member> getInjectedMemberList(Class<?> clazz) {
    if(!injectedMemberCache.containsKey(clazz)) {
      memberCacheLock.lock();
      try {
        if(!injectedMemberCache.containsKey(clazz)) {
          injectedMemberCache.put(clazz, doGetInjectedMemberList(clazz));
        }
      } finally {
        memberCacheLock.unlock();
      }
    }
    
    return injectedMemberCache.get(clazz);
  }
  
  /**
   * Collects a list of members that need injection of an adapter. (Uncached)
   */
  private List<Member> doGetInjectedMemberList(final Class<?> clazz) {
    final List<Member> out = new ArrayList<Member>();
    
    // field annotation and type scan
    ReflectionUtils.doWithFields(clazz, new FieldCallback() {
      @Override
      public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
        AutowiredAdapter anno = field.getAnnotation(AutowiredAdapter.class);
        if(anno != null) {
          if (!field.getType().isAssignableFrom(SpringActiveRecordAdapter.class)) {
            throw new IllegalStateException("AutowiredAdapter annotation requires a field of type SpringActiveRecordAdapter: " + field);
          }
          
          out.add(field);
        }
      }
    });
    
    // method annotation and type scan
    ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
      public void doWith(Method method) {
        AutowiredAdapter anno = method.getAnnotation(AutowiredAdapter.class);
        if(anno != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
          if (Modifier.isStatic(method.getModifiers())) {
            throw new IllegalStateException("AutowiredAdapter annotation is not supported on static methods");
          }
          
          Class<?>[] types = method.getParameterTypes();
          if (types.length != 1 || !types[0].isAssignableFrom(SpringActiveRecordAdapter.class)) {
            throw new IllegalStateException("AutowiredAdapter annotation requires one argument of type SpringActiveRecordAdapter: " + method);
          }
          
          out.add(method);
        }
      }
    });
    
    return out;
  }

}
